package com.yupi.yuojcodesandbox;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ArrayUtil;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.yupi.yuojcodesandbox.model.ExcuteCodeRequest;
import com.yupi.yuojcodesandbox.model.ExcuteCodeResponse;
import com.yupi.yuojcodesandbox.model.ExcuteMessage;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class JavaDockerCodeSandbox extends JavaCodeSandboxTemplate {
    private static final long TIME_OUT = 5000L;
    public static final boolean FIRST_INIT = true;

    public static void main(String[] args) {
        JavaDockerCodeSandboxOld javaNativeCodeSandbox = new JavaDockerCodeSandboxOld();

        ExcuteCodeRequest excuteCodeRequest = new ExcuteCodeRequest();
        //因为程序接受的参数有两组 所以控制台会有两次运行成功的输出
        excuteCodeRequest.setInputList(Arrays.asList("1 2", "1 3"));
        //ResourceUtil.readStr可以读取resources目录下的文件 也就是存放用户代码的目录 然后为测试用例的excuteCodeRequest.setCode()赋值
//        String code = ResourceUtil.readStr("testCode/simpleCompute/Main.java", StandardCharsets.UTF_8);

        //测试用户输入参数的测试
        String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);


        //测试恶意程序
//        String code = ResourceUtil.readStr("testCode/unsafeCode/RunFileError.java", StandardCharsets.UTF_8);
        excuteCodeRequest.setCode(code);
        excuteCodeRequest.setLanguage("java");
        ExcuteCodeResponse excuteCodeResponse = javaNativeCodeSandbox.excuteCode(excuteCodeRequest);
        System.out.println(excuteCodeResponse);
    }
    @Override
    public List<ExcuteMessage> runFile(File userCodeFile, List<String> inputList) {
        //3把编译好的文件上传到容器环境内 创建容器，上传编译文件
        DockerClient dockerClient = DockerClientBuilder.getInstance().build();

        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();

        //定义拉取的镜像alpine表示轻量的版本
        String image = "openjdk:8-alpine";
        //第一次执行需要拉取镜像 往后就不要了 所以定义一个boolean类型去判断是否存在镜像的初始化 如果不存在就创建镜像 存在则不创建镜像了
        if(FIRST_INIT){
            //拉取镜像
            //使用docker拉取镜像
            PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image);
            //回调函数
            PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
                @Override
                public void onNext(PullResponseItem item) {
                    //没下载一个阶段都会打印一次
                    System.out.println("下载镜像：" + item.getStatus());
                    //onNext表示镜像每下载一个阶段就会触发一次onNext方法 用异步就是因为下载的时间可能非常长不可能等全部下载完 所以是一段一段执行的
                    super.onNext(item);
                }
            };
            try {
                pullImageCmd
                        //exec是执行操作 参数是一个回调用于得知镜像拉取之后需要做的事情 且是异步的
                        .exec(pullImageResultCallback)
                        //awaitCompletion的作用就是阻塞 等到镜像下载完成才会执行下一步 程序如果没有下载完就会一直卡在这里
                        .awaitCompletion();
            } catch (InterruptedException e) {
                System.out.println("拉取镜像异常");
                throw new RuntimeException(e);
            }
            System.out.println("下载完成");
        }

        //创建容器 把编译过后的文件放到容器里面（创建容器的时候就把文件放到容器里面）
        CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image);
        //把编译过后的文件放到容器里面（创建容器的时候就把文件放到容器里面）
        HostConfig hostConfig = new HostConfig();
        //Bind有两个参数 一个是本地的文件的路径 一个是容器内部存放文件的路径 就是做了一个映射 后面在容器里面执行的代码就是/app下的代码 /app路径是在虚拟机的容器里面的不是在虚拟机里面
        hostConfig.setBinds(new Bind(userCodeParentPath,new Volume("/app")));
        //设置内存的限制
        hostConfig.withMemory(100*1000*1000L);
        //设置内存的交换为0 可以一定程序上减少内存的值写入硬盘照成资源的浪费
        hostConfig.withMemorySwap(0L);
        //是一个用于 Linux 内核的安全功能，它允许你限制进程可以执行的系统调用，从而减少潜在的攻击面和提高容器的安全性，
        String profileConfig = ResourceUtil.readUtf8Str("profile.json");
        hostConfig.withSecurityOpts(Arrays.asList("seccomp=" + profileConfig));
        //设置CPU
        hostConfig.withCpuCount(1L);
        CreateContainerResponse createContainerResponse = containerCmd
                //创建一个 可交互 的容器，能接受多次输入并且输出。不然每次都单独创建一个容器浪费资源
                //3个命令的作用是把docker容器和本地终端进行连接 能够获取到输入以及在终端进行输出 创建一个可交互的容器
                .withAttachStdin(true)
                .withAttachStderr(true)
                .withAttachStdout(true)
                //设置一个交互终端
                .withTty(true)
                //把编译过后的文件放到容器里面（创建容器的时候就把文件放到容器里面）
                .withHostConfig(hostConfig)
                //限制用户访问网络
                .withNetworkDisabled(true)
                //限制用户不能向 root 根目录写文件
                .withReadonlyRootfs(true)
                .exec();
        System.out.println(createContainerResponse);
        String containerId = createContainerResponse.getId();
        System.out.println("容器的ID:" + containerId);


        //4.启动容器
        StartContainerCmd startContainerCmd = dockerClient.startContainerCmd(containerId);
        startContainerCmd.exec();


        //5.在容器中执行代码，得到输出结果 docker操作容器执行用户的java编译后的代码
        //docker exec keen_blackwell java -cp /app Main 1 3
        //withCmd的参数是一个数组 ArrayUtil.append可以在数组原有的字符串后面添加字符串 假如现在进来的一组参数为1 3 就执行docker exec keen_blackwell java -cp /app Main 1 3
        //下一组 2，3就执行docker exec keen_blackwell java -cp /app Main 2 3
        List<ExcuteMessage> excuteMessageList = new ArrayList<>();

        for (String inputArgs : inputList) {
            StopWatch stopWatch = new StopWatch();
            String[] inputArgsArray = inputArgs.split(" ");
            String[] cmdArray = ArrayUtil.append(new String[]{"java","-cp","/app","Main"},inputArgsArray);
            //execCreateCmd构建执行的命令 参数为容器的id 这样在拼接命令的时候就不需要指定容器的名称了
            ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                    //参数为命令的数组
                    .withCmd(cmdArray)
                    .withAttachStderr(true)
                    .withAttachStdin(true)
                    .withAttachStdout(true)
                    .exec();
            System.out.println("创建执行的命令为：" + execCreateCmdResponse.getRawValues());
            ExcuteMessage excuteMessage = new ExcuteMessage();
            final String[] message = {null};
            final String[] errorMessage = {null};
            Long time = 0L;
            String execId = execCreateCmdResponse.getId();
            //一开始的时候设置默认就是超时
            final boolean[] timeout = {true};
            ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() {
                //当程序执行完成的时候会执行这个方法
                //如果程序在5秒之内完成了就会触发onComplete方法 然后把timeout改成false设置为没有超时 然后最后通过timeout的值就可以判断程序是否超时
                @Override
                public void onComplete() {
                    timeout[0] = false;
                    super.onComplete();
                }

                @Override
                public void onNext(Frame frame) {
                    StreamType streamType = frame.getStreamType();
                    //从frame中获取执行的结果信息是错误还是正确与其对应的枚举值进行比较 错误的输出错误信息 正常输出正常信息
                    if (StreamType.STDERR.equals(streamType)) {
                        message[0] = new String(frame.getPayload());
                        System.out.println("输出错误结果：" + message[0]);
                    } else {
                        errorMessage[0] =  new String(frame.getPayload());
                        System.out.println("输出正常结果：" + errorMessage[0]);
                    }
                    super.onNext(frame);
                }
            };
            final long[] maxMemory = {0L};
            // 获取占用的内存  先启动监控在启动程序 因为一次循环的一个程序的的内存
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {

                @Override
                public void onStart(Closeable closeable) {

                }

                @Override
                public void onNext(Statistics statistics) {
                    // 获取占用的内存
                    System.out.println("内存占用：" + statistics.getMemoryStats().getUsage());
                    maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
                }

                @Override
                public void close() throws IOException {

                }


                @Override
                public void onError(Throwable throwable) {

                }

                @Override
                public void onComplete() {

                }
            });
            // 获取占用的内存
            statsCmd.exec(statisticsResultCallback);

            //执行构建的命令 是异步的
            try {
                //在正式开始执行前开始计时
                stopWatch.start();
                dockerClient.execStartCmd(execId)
                        .exec(execStartResultCallback)
                        //对程序的执行时间进行限制 当超过TIME_OUT的时候程序接着往下执行而不是一直卡在这里
                        //但是，这种方式无论超时与否，都会往下执行，无法判断是否超时。可以定义一个标志，如果程序执行完成，把超时标志设置为 false。
                        //在程序执行的回调函数execStartResultCallback里面设置标志 根据计算程序的花费时间和超时时间进行对比 如果在超时时间内就表示没有超时程序正常往下走
                        .awaitCompletion(TIME_OUT, TimeUnit.MICROSECONDS);
                //在执行完毕之后停止计时
                stopWatch.stop();
                time = stopWatch.getLastTaskTimeMillis();
                //记得关闭计算内容
                statsCmd.close();
            } catch (InterruptedException e) {
                System.out.println("docker执行容器里面用户编译过后的代码异常");
                throw new RuntimeException(e);
            }
            excuteMessage.setErrorMessage(errorMessage[0]);
            excuteMessage.setMessage(message[0]);
            //在这里是每一轮docker执行容器代码的时间 最后在遍历excuteMessageList的时候才计算最大的时间放到excuteCodeResponse里面的time属性
            excuteMessage.setTime(time);
            //这里获取到最大值是因为在一次循环里面的内存本身就是多个的 而时间本来就是一个 所以最后如果有两次循环我们就获取到两次最大值的内存 然后在对比两次最大值取一个更大的
            //也有可能因为输出结果就只有一个就代表第一次循环记录了一个内存大小 第二次循环的内存大小和第一次进行比较 取最大的进行输出
            excuteMessage.setMemory(maxMemory[0]);
            excuteMessageList.add(excuteMessage);
        }
        return excuteMessageList;
    }

    @Override
    public ExcuteCodeResponse excuteCode(ExcuteCodeRequest excuteCodeRequest) {
        return super.excuteCode(excuteCodeRequest);
    }
}
